/**
 *  
 */
package research.sbookmarking.io;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.afox.util.RMethod;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.connotea.Connotea;
import org.connotea.ConnoteaException;
import org.connotea.Post;
import org.restlet.data.Reference;

import research.sbookmarking.AppController;
import research.sbookmarking.DataContainer;

//
/**
 * This is an abstract class. Each child is required to implement the {@link execute} method.  This class
 * conntains the command that will be executed against Connotea.org 
 * <p> 
 * @author <A HREF="mailto:aatleung@gmail.com">Albert Leung</a>
 * @version 1.0
 * 
 */
public abstract class Command {
	
	private DataContainer result = null;
	private List<String> commands = null;
	private String queryName = null;
	private StringBuffer timeline = new StringBuffer("Data is from");
	private String queryFilename; 
	// by default the result of the query will not be saved in DataContainer
	//private boolean saveResults = false;
	
	//the default starting value to retrieve at
	private int start=0;
	//the default maximum number records to retrieve each query
	private int limit=10;
	
	protected Command (String filename, String queryName){
		setQueryFilename(filename);
		setQueryName(queryName);
	}
	
	/**
	 * This constructor can only be called by its subclass.
	 * <p> 
	 * @param queryName contains the type of query to be execute against Connotea.org.
	 * @param commands contains the list of parameters that will be query. 
	 */
	protected Command (String filename, String queryName, List<String> commands){
		setQueryFilename(filename);
		setQueryName(queryName);
		this.commands = commands;
	}


	/**
	 * This constructor can only be called by its subclass.
	 * <p> 
	 * @param queryName contains the type of query to be execute against Connotea.org.
	 * @param commands contains the list of parameters that will be query. 
	 * @param limit contains the maximum number of records that query each time. 
	 * @param start contains the starting record to start querying from.
	 */
	protected Command (String filename, String queryName, List<String> commands, Integer limit, Integer start){
		setQueryFilename(filename);
		setQueryName(queryName);
		setCommands(commands);
		setLimit(limit);
		setStart(start);
	}

	private void createTimeline(Date date){
		DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM);
		if (timeline.lastIndexOf(dateFormat.format(date))==-1){
			timeline.append(" ");
			timeline.append(dateFormat.format(date));
		}
	}
	
	private void setQueryFilename(String filename){
		this.queryFilename=filename;
	}
	protected String getQueryFilename(){

		return queryFilename;
	}
	
	protected void appendTimeline(String string){
		timeline.append(string);
	}
	
	protected String getTimeline(){
		return timeline.toString();
	}
	
	protected String getQueryName() {
		return this.queryName;
	}

	protected void setQueryName(String queryName){
		this.queryName = queryName;
	}

	protected List<String> getCommands() {
		return commands;
	}
	
	protected void setCommands(List<String> commands){
		this.commands = commands;
	}

	protected int getLimit(){
		return limit;
	}
	
	protected void setLimit(Integer limit){
		this.limit = limit.intValue();
	}
	
	protected int getStart(){
		return start;
	}

	protected void setStart(Integer start){
		this.start = start.intValue();
	}

	/**
	 * This method needs to be implement in its children.  This method contains the procedures
	 * required to query Connotea.org 
	 */
	public abstract void execute();
	
	/**
	 * Returns the results from the data query of Connotea.org
	 * @return a {@link DataContainer} that contains the data extracted from Connotea.org.
	 */
	public DataContainer getResult() {
		return result;
	}

	protected void setResult(DataContainer data) {
		this.result=data;
	}
	
	//This method should only be called by subclass and nothing else
	public List<Post> processURL(Connotea database, String url, Integer limit, Integer start)
		throws ConnoteaException{
		return database.getPosts(new Reference(url), limit.intValue(), start.intValue());
	}
	
	//This method should only be called by subclass and nothing else
	public Object processUser(Connotea database, String username, Integer limit, Integer start)
		throws ConnoteaException{
		return database.getPosts(username, limit.intValue(), start.intValue());
	}
	
	//This method should only be called by subclass and nothing else
	public List<Post> processTag (Connotea database, String subject, Integer limit, Integer start)
		throws ConnoteaException {
		return database.getPosts(new org.connotea.Tag(subject), limit.intValue(), start.intValue());
	}
	
	//This method should only be called by subclass and nothing else
	public List<Post> processDate (Connotea database, String date, Integer limit, Integer start) 
		throws java.util.zip.DataFormatException , ConnoteaException{
		SimpleDateFormat dateFormat = new SimpleDateFormat ("yyyy-MM-dd");
		Date aDate;
		
		try {
			aDate = dateFormat.parse(date);

			if (aDate == null) {
				dateFormat = new SimpleDateFormat ("yyyy-MMM-dd");
				aDate = dateFormat.parse(date);
				if (aDate == null) {
					throw new java.util.zip.DataFormatException ("Invalid date format. Please uses the following " +
							"form YYYY-MMM-DD or YYYY-MM-DD");
				}
				
			}
			createTimeline(aDate);
			return database.getPosts(aDate, limit.intValue(), start.intValue());
		}
		catch (java.text.ParseException e){
			throw new RuntimeException(e);
		}
	}

	private ArrayList<Object> error (int prev, int current, Exception x){
		ArrayList<Object> values = new ArrayList<Object>();
		
		if (current==prev){
			System.out.println ("Error occured so getting the next set of data. Record: "+
					current +" :"+x.getMessage());

			values.add(Boolean.valueOf(true));
			values.add(Integer.valueOf(++current));

			Logger logger = AppController.getInstance().getLogger();
			logger.log(Level.FINEST,"Error Message: Record: " + start + x.getMessage());
			logger.log(Level.FINEST,"Cause: " + x.getCause()+"");
			logger.log(Level.FINE,AppController.newline);
			//logger.log(Level.FINEST, Arrays.asList(exception.getStackTrace()).toString());

		}
		else {
			values.add(Boolean.FALSE);
			values.add(Integer.valueOf(current));
		}

		values.add(Boolean.TRUE);
		values.add(Integer.valueOf(current));
		
		return values;
	}
	
	protected int getAll (Connotea database, String timeQuery, String query, int limit, int start,
			HSSFWorkbook wb, HSSFSheet sheet, int rowNo){
		return getAll(new ArrayList<Object>(), database,timeQuery,query,limit,start, wb,sheet,rowNo);
	}
	
	@SuppressWarnings("unchecked")
	//protected int getAll (Connotea database, DataContainer container, String query, int limit, int start,
	protected int getAll (List results, Connotea database, String timeQuery, String query, int limit, int start,
		HSSFWorkbook wb, HSSFSheet sheet, int rowNo){
		Logger logger = AppController.getInstance().getLogger();

		int totalNum = 0;
		int retrieved = 0;
		int prevNum=-1;
		int oldLimit=limit;
		boolean error=false;
		boolean doubled=false;
		ArrayList<Object> param = new ArrayList<Object>();
		List<Post> temp = new ArrayList<Post>();
		
		OutputController oc = OutputController.getInstance();
		String filename = "data/raw/" + timeQuery + "-" + query +"-posts-all.raw.gz";
		String filename1 = "data/rtf/" + timeQuery + "-" + query +"-posts-all.rtf.gz";
		String filename2 = "data/dsv/" + timeQuery +".txt.gz";

		do {
			try {
				if ((error) && (doubled)){

					limit=oldLimit;
					error=false;
					doubled=false;
				}
	
				
				param.add(database);
				param.add(query);
				param.add(new Integer(limit));
				param.add(new Integer(start));

				temp = (List<Post>)RMethod.invoke(this, "process"+getQueryName(), param);
				AppController.getInstance().copy(results,temp);
				param.clear();

				//rowNo=oc.saveExcelFile(wb, sheet, rowNo, temp);

				retrieved += temp.size();
				totalNum = temp.size() + start;
				
				oc.saveObjectData(filename, temp,true);
				oc.saveObjectData(filename, Integer.valueOf(retrieved), true);
				//container.setQueryResults(temp);
				//param.add(container);
				oc.saveRawData(filename1, temp, true);
				oc.saveDsvData(filename2, temp, true);
				//ProcessController.getInstance().processData(container);
				
				if (oldLimit==temp.size()){
					if (retrieved%100==0){
						System.out.println ("No of records received for " + query + ": " + 
								retrieved + " out of " + totalNum);
					}

					start+=limit;
				}
			}
			catch (java.lang.OutOfMemoryError x) {
				logger.log(Level.SEVERE,"Not enough memmory: "+ x.getCause());
				System.exit(-1);
			}
			catch (NullPointerException x){
				logger.log(Level.FINEST,"Exception detail"+
						AppController.newline + x);

				logger.log(Level.FINE,AppController.newline);
				
				ArrayList<Object> result = error(prevNum,start,x);
				doubled = ((Boolean)result.get(0)).booleanValue();
				start = ((Integer)result.get(1)).intValue();
				error = ((Boolean)result.get(2)).booleanValue();
				prevNum = ((Integer)result.get(3)).intValue();
			}
			catch (Exception x) {
				//System.out.println ("Error Message: " + e.getMessage());
				ArrayList<Object> result = error(prevNum,start,x);
				doubled = ((Boolean)result.get(0)).booleanValue();
				start = ((Integer)result.get(1)).intValue();
				error = ((Boolean)result.get(2)).booleanValue();
				prevNum = ((Integer)result.get(3)).intValue();
			}
			finally{
				param.clear();
			}
			
		} while (temp.size()>=limit);

		oc.closeStream("ObjectOutputStream", filename);
		oc.closeStream("DataOutputStream", filename1);
		
		System.out.println ("Total no. of records received for " + query + ": " + 
				retrieved + " out of " + totalNum);

		return retrieved;
	}
	
	protected HSSFWorkbook setupExcelFile(OutputController oc, String name){
		HSSFWorkbook workbook = new HSSFWorkbook();
		HSSFSheet sheet = workbook.createSheet(name);
			
		ArrayList<Object> headings =  new ArrayList<Object>();
		String[]header = {"Data retrieved is in " + TimeZone.getDefault().getDisplayName() + " timezone."};
		AppController.getInstance().copy(headings,Arrays.asList(header).iterator());
		oc.setupExcelFile(sheet, headings, 0);
		headings.clear();
		String[]head = {"Date (GMT)", "URL", "Posted by", "Title", "Description", "Tag(Subject)", 
				"Own Work", "Citation Present", "Last Updated (GMT)"};
		AppController.getInstance().copy(headings,Arrays.asList(head).iterator());
		oc.setupExcelFile(sheet, headings, 1);
		sheet.createFreezePane( 0, 2, 0, 2 );
		return workbook;
	}
	protected String createHeader(){
		//return "Date (GMT) \t URL\t Posted by \t URL Title \t URL Description \t Tags (Subjects) \t Linked own work\t Citation present \t Last Updated(GMT)";
		return "Date (GMT) \t URL\t Posted by \t Tags (Subjects) \t Linked to own work\t Citation present \t Last Updated(GMT)";
	}
	
	protected String createNotes (){
		StringBuffer notes = new StringBuffer("Date was queryed in " +TimeZone.getDefault().getDisplayName() + " but time is displayed in GMT");
		notes.append(AppController.newline);
		notes.append("\"Posted by\" refers to the user who bookmarked this URL.");
		notes.append(AppController.newline);
		notes.append("\"Tags (Subjects)\" refers to the keywords the user used to describe this URL.");
		notes.append(AppController.newline);
		notes.append("\"Linked to Own\" refers to whether the URL links to a work of the user");
		notes.append(AppController.newline);
		notes.append("\"Citation present\" refers to whether the posting has a citation record");
		notes.append(AppController.newline);
		notes.append("\"Last Updated\" refers to the last date this posting was modified since a user can only bookmark the same URL once");
		return notes.toString();
	}
}
